package com.rao.gmall.gateway.component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rao.gmall.common.result.Result;
import com.rao.gmall.common.result.ResultCodeEnum;
import com.rao.gmall.common.util.IpUtil;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.Set;

/**
 * 网关对所有请求认证的过滤器
 * <p>
 * 页面  --- >  网关  ----> 转给服务
 * 内部 服务 ----》 服务
 *
 *
 *
 */
@Data
@ConfigurationProperties(prefix = "authinfo")
@Component
public class AuthFilter implements GlobalFilter {

    // 精确匹配器
    private AntPathMatcher matcher = new AntPathMatcher();

    private String needLoginPathPattern = "/api/**/auth/**";

//    @Value("${authinfo.urls}")  //简单取值
    /**
     * @ConfigurationProperties 绑定属性 比 @Value 强大
     * 支持复杂封装。
     */
    private Set<String> urls;

    @Autowired
    StringRedisTemplate redisTemplate;


    /**
     * Webflux的编程方式（响应式编程）【0/1，  多个N 】
     * - Mono： 返回0个或者一个数据
     * - FLux： 返回多个数据
     *
     * @param exchange 包含了request和respone
     * @param chain    filter链  chain.doFilter(request,response,chain)
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1、获取到请求响应
        ServerHttpRequest request = exchange.getRequest(); //请求不能篡改
        ServerHttpResponse response = exchange.getResponse(); //响应能改

        //所有请求
        //1、内部接口，拒绝访问   /ajk/djkj/innerabc

        String path = request.getURI().getPath();
        if (matcher.match("/**/inner/**", path)) {
            //拒绝访问 PERMISSION(209, "没有权限"),
            return responseWriteData(response, ResultCodeEnum.PERMISSION);
        }

        //2、判断用户是否登录？如果登录了，再检查是否合法
        //获取到当前用户id， null， -1,  真正的id
        String userId = getUserId(request);
        if("-1".equals(userId)){
            //登录了但是不合法
            return responseWithLocation(request,response);
        }


        /**
         * 以下是需要登录的，就由网关打到登录页
         */
        //能走到这里，一定不是-1;  null，有值
        //3、系统规范  /api/**/auth/**，这些接口需要登录
        if (matcher.match(needLoginPathPattern, path) && StringUtils.isEmpty(userId)) {
            //需要登录，但是前面返回的是null，没有登录。返回登陆界面
            return responseWithLocation(request,response);
        }

        //4、为了增强扩展性，希望有一个配置，直接配置哪些需要登录，网关就自己校验
        if (urls.contains(path) && StringUtils.isEmpty(userId)) {
            //配置的请求需要登录，实际未登录
            return responseWithLocation(request,response); // 响应结束
        }

        ServerHttpRequest.Builder requestBuilder = request.mutate();

        String userTempId = getUserTempId(request);
        if (!StringUtils.isEmpty(userTempId)){
            requestBuilder.header("userTempId", userTempId);
        }
        // 透传userId， 给要放行的 request 的请求头上设置加一个字段 userId: 12，
        // 请求再放行给其他微服务.  要么null 要么真的用户id
        if(!StringUtils.isEmpty(userId)){
            //添加自定义userId请求头。原来的请求不能直接修改
//            HttpHeaders headers = request.getHeaders();
//            headers.set("userId",userId);

            requestBuilder.header("userId", userId);
        }
        ServerHttpRequest build = requestBuilder.build();

        ServerWebExchange exchangeMutate = exchange.mutate().request(build).response(response).build();

            return chain.filter(exchangeMutate);

        //过滤器链继续往下
//        return chain.filter(exchange);
    }


    /**
     * 重定向到登录页
     * @param request
     * @param response
     * @return
     */
    private Mono<Void> responseWithLocation(ServerHttpRequest request ,ServerHttpResponse response) {
        //让浏览器自己重定向到登录页
        response.setStatusCode(HttpStatus.FOUND);
        //浏览器一看状态吗是302，自己解析响应头的 Location字段，浏览器自己跳过去

        HttpHeaders headers = response.getHeaders();
        String string = request.getURI().toString();
        String url = "http://passport.gmall.com/login.html?originUrl="+string;
        headers.set(HttpHeaders.LOCATION,url);

        // 登录的是错误的，需要清除以前的登录cookie
        // 1、服务器命令浏览器-保存cookie，响应头会带  Set-Cookie:  token=xxxxx  默认时效，session
        // response.addCookie(XXXX);
        // 2、删除cookie.  Set-Cookie:  token=xxxxx  默认时效，0
        // 3、命令浏览器保存cookie的时候，如果时间>0，浏览器保存。如果=0，浏览器就销毁

        // 删除错误 token
        ResponseCookie newToken = ResponseCookie.from("token", "")
                .maxAge(0L) // 最大存活时间，时间到达后自动清除
                .domain("gmall.com") // 生命作用域
                .build();
        // 将新设置的 token 保存到浏览器端 cookie 中
        response.addCookie(newToken);

        // 删除错误 userInfo
        ResponseCookie newUserInfo = ResponseCookie.from("userInfo", "")
                .maxAge(0L)
                .domain("gmall.com")
                .build();
        // 将新设置的 userInfo 保存到浏览器端 cookie 中
        response.addCookie(newUserInfo);

        return response.setComplete();
    }

    private String getUserTempId(ServerHttpRequest request) {
        //如果用户未登录，就使用临时 id。同时要进行合法性校验。为了兼容ajax
        String userTempId = request.getHeaders().getFirst("userTempId");
        if (StringUtils.isEmpty(userTempId)){
            //正常请求头的token是空的。 安卓，ios
            HttpCookie cookie = request.getCookies().getFirst("userTempId");
            if (cookie != null){
                userTempId = cookie.getValue();
            }
        }
        return userTempId;
    }

    private String getUserId(ServerHttpRequest request) {
        //如果用户登录了，就自己返回用户id。同时要进行合法性校验。
        String token = request.getHeaders().getFirst("token");
        if(StringUtils.isEmpty(token)){ //兼容移动端
            //正常请求头的token是空的。 安卓，ios
            HttpCookie cookie = request.getCookies().getFirst("token");
            if(cookie!=null){
                token = cookie.getValue();
            }
        }


        //拿到了token，但是为null，说明从未登录
        if(StringUtils.isEmpty(token)){
            return null;
        }else {
            //验证合法性。  验证jwt的合法性和解码
            String key = "user:login:"+token;  //就能去redis查到用户信息
            String json = redisTemplate.opsForValue().get(key);
            if(StringUtils.isEmpty(json)){
                return "-1";
            }
            Map<String, String> map = null;
            try {
                map = new ObjectMapper().readValue(json, new TypeReference<Map<String, String>>() {
                });
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }

            //拿到redis中的用户id
            String userId = map.get("userId");
            String ipAddr = map.get("ipAddr");
            //防冒牌
            String ipAddress = IpUtil.getGatwayIpAddress(request);
            if(ipAddr.equals(ipAddress)){
                return userId;
            }else {
                return "-1";
            }


        }


    }

    /**
     * 给浏览器响应没有权限的json
     * @param response
     * @param permission
     * @return
     */
    private Mono<Void> responseWriteData(ServerHttpResponse response, ResultCodeEnum permission) {
        /**
         * {
         *     code: 209
         *     message: "没有权限"
         * }
         */
        response.setStatusCode(HttpStatus.OK);
        //给response响应体放json串
        HttpHeaders headers = response.getHeaders();
//       headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf-8");


        //普通编程。数据库查1号数据。  Person person = dao.findById(1);
        //数据库数据做成一个 Publisher。非阻塞  Mono  Flux
        //  dao.findById(1) == Publisher;  =======   Consumer(person)->{}
        // ajax 回调机制

        DataBufferFactory dataBufferFactory = response.bufferFactory();

        //构造我们的数据
        Result<Object> build = Result.build(null, permission);
        String s = null;
        try {
            //将result转为json
            s = new ObjectMapper().writeValueAsString(build);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        //将json写出去


        //返回流式数据
        return response.writeWith(Mono.just(dataBufferFactory.wrap(s.getBytes())));
    }
}
